package ar.com.hjg.pngj.chunks;

import java.io.ByteArrayInputStream;
import java.io.OutputStream;
import java.util.zip.CRC32;

import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjBadCrcException;
import ar.com.hjg.pngj.PngjException;
import ar.com.hjg.pngj.PngjOutputException;

/**
 * Raw (physical) chunk.
 * <p>
 * Short lived object, to be created while serialing/deserializing Do not reuse it for different chunks. <br>
 * See http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html
 */
public class ChunkRaw {
  /**
   * The length counts only the data field, not itself, the chunk type code, or the CRC. Zero is a valid length.
   * Although encoders and decoders should treat the length as unsigned, its value must not exceed 231-1 bytes.
   */
  public final int len;

  /**
   * A 4-byte chunk type code. uppercase and lowercase ASCII letters
   */
  public final byte[] idbytes;
  public final String id;

  /**
   * The data bytes appropriate to the chunk type, if any. This field can be of zero length. Does not include crc. If
   * it's null, it means that the data is ot available
   */
  public byte[] data = null;
  /**
   * @see ChunkRaw#getOffset()
   */
  private long offset = 0;

  /**
   * A 4-byte CRC (Cyclic Redundancy Check) calculated on the preceding bytes in the chunk, including the chunk type
   * code and chunk data fields, but not including the length field.
   */
  public byte[] crcval = new byte[4];

  private CRC32 crcengine; // lazily instantiated

  public ChunkRaw(int len, String id, boolean alloc) {
    this.len = len;
    this.id = id;
    this.idbytes = ChunkHelper.toBytes(id);
    for (int i = 0; i < 4; i++) {
      if (idbytes[i] < 65 || idbytes[i] > 122 || (idbytes[i] > 90 && idbytes[i] < 97))
        throw new PngjException("Bad id chunk: must be ascii letters " + id);
    }
    if (alloc)
      allocData();
  }

  public ChunkRaw(int len, byte[] idbytes, boolean alloc) {
    this(len, ChunkHelper.toString(idbytes), alloc);
  }

  public void allocData() { // TODO: not public
    if (data == null || data.length < len)
      data = new byte[len];
  }

  /**
   * this is called after setting data, before writing to os
   */
  private void computeCrcForWriting() {
    crcengine = new CRC32();
    crcengine.update(idbytes, 0, 4);
    if (len > 0)
      crcengine.update(data, 0, len); //
    PngHelperInternal.writeInt4tobytes((int) crcengine.getValue(), crcval, 0);
  }

  /**
   * Computes the CRC and writes to the stream. If error, a PngjOutputException is thrown
   * 
   * Note that this is only used for non idat chunks
   */
  public void writeChunk(OutputStream os) {
    writeChunkHeader(os);
    if (len > 0) {
      if (data == null)
        throw new PngjOutputException("cannot write chunk, raw chunk data is null [" + id + "]");
      PngHelperInternal.writeBytes(os, data, 0, len);
    }
    computeCrcForWriting();
    writeChunkCrc(os);
  }

  public void writeChunkHeader(OutputStream os) {
    if (idbytes.length != 4)
      throw new PngjOutputException("bad chunkid [" + id + "]");
    PngHelperInternal.writeInt4(os, len);
    PngHelperInternal.writeBytes(os, idbytes);
  }

  public void writeChunkCrc(OutputStream os) {
    PngHelperInternal.writeBytes(os, crcval, 0, 4);
  }

  public void checkCrc() {
    int crcComputed = (int) crcengine.getValue();
    int crcExpected = PngHelperInternal.readInt4fromBytes(crcval, 0);
    if (crcComputed != crcExpected)
      throw new PngjBadCrcException("chunk: " + this.toString() + " expected=" + crcExpected
          + " read=" + crcComputed);
  }

  public void updateCrc(byte[] buf, int off, int len) {
    if (crcengine == null)
      crcengine = new CRC32();
    crcengine.update(buf, off, len);
  }

  ByteArrayInputStream getAsByteStream() { // only the data
    return new ByteArrayInputStream(data);
  }

  /**
   * offset in the full PNG stream, in bytes. only informational, for read chunks (0=NA)
   */
  public long getOffset() {
    return offset;
  }

  public void setOffset(long offset) {
    this.offset = offset;
  }

  public String toString() {
    return "chunkid=" + ChunkHelper.toString(idbytes) + " len=" + len;
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((id == null) ? 0 : id.hashCode());
    result = prime * result + (int) (offset ^ (offset >>> 32));
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    ChunkRaw other = (ChunkRaw) obj;
    if (id == null) {
      if (other.id != null)
        return false;
    } else if (!id.equals(other.id))
      return false;
    if (offset != other.offset)
      return false;
    return true;
  }

}
